#define USE_16_BIT_STRING_LENGTH
// #define COMPILED_SERIALIZER_SUPPORT
#define PROP_COMPATIBLE
// #define DEBUG_SERIALIZE
// #define SIXTEEN_BIT_CACHE
// #define USE_ADDITIONAL_TYPES
// #define SERIALIZE_NON_PUBLIC

using System;
using System.Runtime.InteropServices;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.IO;
using System.Runtime.Serialization;
using System.Diagnostics;

namespace AltSerialize
{
    public partial class AltSerializer : IDisposable
    {
        public bool SerializeProperties = true;// HEREHERE: false;
        public Encoding Encoding = Encoding.UTF8;
        public static string defaultTypePrefix = "BrainTechLLC.";

        /// <summary>
        /// If true, the names of the properties are serialized along with
        /// the data.  This ensures that previous versions of serialized data
        /// don't misread the data stream.
        /// </summary>
        public bool SerializePropertyNames = false;

        /// <summary>
        /// Gets or sets the stream used for serialization
        /// or deserialization.
        /// </summary>
        public Stream Stream;

        public static List<ReflectedMemberInfo> EmptyProperties = new List<ReflectedMemberInfo>();

        /// <summary>
        /// Gets or sets a value indicating whether or not the serializer
        /// cache is enabled.  If true, then any duplicate objects that are serialized
        /// are hashed and stored, minimizing the amount of space serialization takes.
        /// </summary>
        public bool CacheEnabled = false;

        private static object _lockObj = new object();
        private static bool _initialized = false;

        private static Dictionary<string, Type> _types = new Dictionary<string, Type>();
        private static Dictionary<Type, ObjectMetaData> MetaDataHash = new Dictionary<Type, ObjectMetaData>();
        private static Dictionary<Type, ReflectedMemberInfo[]> FieldsLookup = new Dictionary<Type, ReflectedMemberInfo[]>();

        public AltSerializer() : this(new MemoryStream()) { }
        public AltSerializer(byte[] bytes) : this(new MemoryStream(bytes)) { }

        public AltSerializer(Stream stream)
        {
            InitStaticCache();
            this.Stream = stream;
            arrayBuffer = new byte[BlockSize];
        }

        // Insertion sort
        public void InsertSortedMetaData(List<ReflectedMemberInfo> list, ReflectedMemberInfo minfo)
        {
            for (int i = 0; i < list.Count; i++)
            {
                if (list[i].Name.CompareTo(minfo.Name) > 0)
                {
                    list.Insert(i, minfo);
                    return;
                }
            }
            list.Add(minfo);
        }

        /// <summary>
        /// Gets the cache used by the serializer.
        /// </summary>
        public SerializerCache Cache = new SerializerCache();

        private byte[] arrayBuffer = null;

        int BlockSize = 16384;

        /// <summary>
        /// Gets the meta data for an object type.  If the Type doesn't exist in
        /// the meta data hash, it is created.
        /// </summary>
        /// <param name="type">The object Type to get metadata for.</param>
        /// <returns>Returns an ObjectMetaData class representing the <paramref name="type"/> parameter.</returns>
        internal ObjectMetaData GetMetaData(Type type)
        {
            if (type == null)
                throw new AltSerializeException("The serializer could not get meta data for the type.");

            ObjectMetaData md;

            if (MetaDataHash.TryGetValue(type, out md))
                return md;

#if COMPILED_SERIALIZER_SUPPORT
            bool hasCompiledSerializerAttribute = ReflectionHelper.CheckHasCompiledSerializerAttribute(type);

            if (hasCompiledSerializerAttribute)
            {
                // Compiled Serializer flag specified.
                ObjectMetaData cmetadata = new ObjectMetaData(this) { ObjectType = type };
                cmetadata.DynamicSerializer = (DynamicSerializer)DynamicSerializerFactory.GenerateSerializer(type);
                MetaDataHash[type] = cmetadata;
                return cmetadata;
            }
#endif

            if (type.CheckImplementsIAltSerializable())
            {
                // This will only be called using interface methods
                ObjectMetaData imetaData = new ObjectMetaData(this) { ObjectType = type, IsIAltSerializable = true };
                MetaDataHash[type] = imetaData;
                return imetaData;
            }

            List<ReflectedMemberInfo> serializedProperties = GetSerializedProperties(type);

            ReflectedMemberInfo[] fieldArray = GetFieldArray(type);

            ObjectMetaData metaData = new ObjectMetaData(this) { ObjectType = type, Fields = fieldArray, Properties = serializedProperties.ToArray() };

            CheckImplementations(type, metaData);

            // Store in hash and return the structure
            MetaDataHash.Add(type, metaData);
            return metaData;
        }

        public List<ReflectedMemberInfo> GetSerializedProperties(Type type)
        {
#if PROP_COMPATIBLE
            List<ReflectedMemberInfo> serializedProperties = new List<ReflectedMemberInfo>();

            // Get all public and non-public properties.
            PropertyInfo[] props = type.GetProperties(BindingFlags.Instance | BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.NonPublic);
            foreach (PropertyInfo info in props)
            {
                if (info.GetCustomAttributes(typeof(DoNotSerializeAttribute), true).Length > 0)
                {
                    // Don't add it to the list if a nonserialized attribute is set
                    continue;
                }
                if (info.GetIndexParameters().Length > 0)
                {
                    continue;
                }
                if (info.CanRead == false || info.CanWrite == false)
                {
                    // If its a read or write only property, don't bother with it
                    continue;
                }
                InsertSortedMetaData(serializedProperties, new ReflectedMemberInfo(info));
            }

            return serializedProperties;
#else
            return EmptyProperties;
#endif
        }

        public ReflectedMemberInfo[] GetFieldArray(Type type)
        {
            // Get all public and non-public fields.
            List<ReflectedMemberInfo> serializedFields;
            ReflectedMemberInfo[] fieldArray = null;

            if (!FieldsLookup.TryGetValue(type, out fieldArray))
            {
                serializedFields = new List<ReflectedMemberInfo>();
                List<FieldInfo> fields = new List<FieldInfo>();
                ReflectionHelper.GetAllFieldsOfType(type, fields);

                foreach (FieldInfo info in fields)
                {
                    if (!info.IsNotSerialized)
                        InsertSortedMetaData(serializedFields, new ReflectedMemberInfo(info));
                }
                fieldArray = serializedFields.ToArray();

                FieldsLookup.Add(type, fieldArray);
            }

            return fieldArray;
        }

        public void CheckImplementations(Type type, ObjectMetaData metaData)
        {
            if (type.IsGenericType)
            {
                metaData.GenericTypeDefinition = type.GetGenericTypeDefinition();
                metaData.GenericParameters = type.GetGenericArguments();
            }

            // Check for an IList implementation.
            Type iface = type.GetInterface("System.Collections.IList", true);

            if (iface != null)
                metaData.ImplementsIList = true;

            // Check for an IDictionary implementation.
            iface = type.GetInterface("System.Collections.IDictionary", true);

            if (iface != null)
                metaData.ImplementsIDictionary = true;

            // Check for a generic List<> implementation.
            if (metaData.GenericTypeDefinition == typeof(List<>))
            {
                metaData.IsGenericList = true;
                metaData.ImplementsIList = false;
                metaData.Extra = type.GetField("_items", BindingFlags.Instance | BindingFlags.NonPublic);
                metaData.SizeField = type.GetField("_size", BindingFlags.Instance | BindingFlags.NonPublic);
                metaData.SerializeMethod = type.GetMethod("ToArray");
            }
        }

        /// <summary>
        /// Sets the Cache ID of an object.  Used in dynamic serialization; this method
        /// is not intended to be called from any other source.
        /// </summary>
        public void SetCachedObjectID(object obj, int cacheID)
        {
            Cache.SetCachedObjectId(obj, cacheID);
        }

        // Writes the serialization flags as a byte.
        private void WriteSerializationFlags(SerializedObjectFlags flags)
        {
            Stream.WriteByte((byte)flags);
        }

        // Reads a byte as serialization flags.
        private SerializedObjectFlags ReadSerializationFlags()
        {
            return (SerializedObjectFlags)Stream.ReadByte();
        }

        // Special method to write Type objects.
        private void WriteType(Type objectType)
        {
            int typeHash;
            if (ReflectionHelper._hashTypeInt.TryGetValue(objectType, out typeHash))
            {
                WriteHashedType(typeHash);
            }
            else
            {
                // Write the full type name.
                byte[] bytes;

                if (objectType.IsGenericType)
                    bytes = Encoding.UTF8.GetBytes(objectType.AssemblyQualifiedName);
                else
                    bytes = Encoding.UTF8.GetBytes(objectType.Name.Replace(defaultTypePrefix, ""));

                WriteStringLength((int)bytes.Length);
                this.Stream.Write(bytes, 0, bytes.Length);
            }
        }

        public Type ResolveType(string typeName)
        {
            Type returnType;

            if (_types.TryGetValue(typeName, out returnType))            
                return returnType;
            
            returnType = Type.GetType(typeName);

            if (returnType == null)
            {
                // Note - get type name failure handling (i.e. silverlight libraries may not match exactly w/non-silverlight equiv)
                returnType = null; // alternate secondary lookup can go here

                if (returnType == null)
                    throw new AltSerializeException("Unable to GetType object type '" + typeName + ".");
            }

            try { _types.Add(typeName, returnType); }
            catch { }

            return returnType;
        }

        // Special method to read Type objects.
        private Type ReadType()
        {
            int strLen = ReadStringLength();

            if (strLen == 0)
            {
                // Hashed type...
                strLen = ReadTypeInt();
                return ReflectionHelper._hashIntType[strLen];
            }
            else
            {
                Type returnType;
                byte[] bytes = new byte[strLen];
                this.Stream.Read(bytes, 0, strLen);
                string typeName = Encoding.UTF8.GetString(bytes, 0, bytes.Length);

                if (_types.TryGetValue(typeName, out returnType))
                    return returnType;

                returnType = ResolveType(typeName);

                _types.Add(typeName, returnType);
                return returnType;
            }
        }

        /// <summary>
        /// Clears the cache and resets the stream.
        /// </summary>
        public void Reset()
        {
            if (this.Stream != null)
                this.Stream.Position = 0;

            Cache.Clear();
        }

        /// <summary>
        /// Caches an object for the serializer.
        /// </summary>
        /// <remarks>The object being cached is permanately cached, and when encountered by the serializer,
        /// is always marked as a reference.</remarks>
        /// <param name="cachedObject">Object to cache.</param>
        public void CacheObject(object cachedObject)
        {
            Cache.CacheObject(cachedObject, true);
        }

        private void CacheType(Type objType)
        {
            GetMetaData(objType);
            CacheObject(objType);
        }

        private void CheckInit()
        {
            lock (_lockObj)
            {
                if (!_initialized)
                {
                    ReflectionHelper.AddTypes();
                    _initialized = true;
                }
            }
        }

        // Creates a cache of static objects.
        private void InitStaticCache()
        {
            if (!_initialized)
                CheckInit();

            Type[] listTypesToCache = { typeof(List<int>), typeof(List<uint>), typeof(List<byte>), typeof(List<sbyte>), typeof(List<short>), typeof(List<ushort>), typeof(List<long>), typeof(List<ulong>), typeof(List<DateTime>), typeof(List<TimeSpan>), typeof(List<decimal>), typeof(List<float>), typeof(List<double>), typeof(List<Guid>) };

            foreach (Type type in listTypesToCache)
                CacheType(type);

#if USE_ADDITIONAL_TYPES
            foreach (Type type in AdditionalTypes)
                CacheType(type);            
#endif
        }

        public void WriteNullString()
        {
            // write max 24-bit to indicate null.
#if USE_16_BIT_STRING_LENGTH
            Stream.WriteUInt16(0xFFFF);
#else
            Stream.WriteUInt24(0xFFFFFF);
#endif
        }

        /// <summary>
        /// Writes a string to the serialization stream.
        /// </summary>
        public void Write(string str)
        {
            if (str == null)
            {
                WriteNullString();
                return;
            }

            // Write the size of the string
            int byteCount = this.Encoding.GetByteCount(str);
            WriteStringLength(byteCount);

            if (byteCount > BlockSize)
            {
                byte[] bytes = this.Encoding.GetBytes(str);
                this.Stream.Write(bytes, 0, bytes.Length);
            }
            else
            {
                this.Encoding.GetBytes(str, 0, str.Length, arrayBuffer, 0);
                this.Stream.Write(arrayBuffer, 0, byteCount);
            }
        }

        /// <summary>
        /// Reads a signed 32-bit integer from the serialization stream.
        /// </summary>
        public Int32 ReadInt32()
        {
            Stream.Read(arrayBuffer, 0, 4);
            return BitConverter.ToInt32(arrayBuffer, 0);
        }

        /// <summary>
        /// Reads an unsigned 32-bit integer from the serialization stream.
        /// </summary>
        public UInt32 ReadUInt32()
        {
            Stream.Read(arrayBuffer, 0, 4);
            return BitConverter.ToUInt32(arrayBuffer, 0);
        }

        /// <summary>
        /// Reads a Byte from the serialization stream.
        /// </summary>
        public int ReadByte()
        {
            return Stream.ReadByte();
        }

        /// <summary>
        /// Reads a string from the serialization stream.
        /// </summary>
        public string ReadString()
        {
            int byteCount = ReadStringLength();

            if (ByteCountIndicatesNull(byteCount))
                return null;

            // For very large arrays, allocate space enough for them individually.
            byte[] byteArray = (byteCount > BlockSize) ? new byte[byteCount] : arrayBuffer;

            Stream.Read(byteArray, 0, byteCount);
            return this.Encoding.GetString(byteArray, 0, byteCount);
        }

        /// <summary>
        /// Reads a signed 16-bit value from the serialization stream.
        /// </summary>
        public short ReadInt16()
        {
            Stream.Read(arrayBuffer, 0, 2);
            return (short)(arrayBuffer[0] + (arrayBuffer[1] << 8));
        }

        /// <summary>
        /// Reads an unsigned 16-bit value from the serialization stream.
        /// </summary>
        public UInt16 ReadUInt16()
        {
            Stream.Read(arrayBuffer, 0, 2);
            return (UInt16)(arrayBuffer[0] + (arrayBuffer[1] << 8));
        }

        /// <summary>
        /// Reads a signed 64-bit value from the serialization stream.
        /// </summary>
        public long ReadInt64()
        {
            Stream.Read(arrayBuffer, 0, 8);
            return BitConverter.ToInt64(arrayBuffer, 0);
        }

        /// <summary>
        /// Reads an unsigned 64-bit value from the serialization stream.
        /// </summary>
        public UInt64 ReadUInt64()
        {
            Stream.Read(arrayBuffer, 0, 8);
            return BitConverter.ToUInt64(arrayBuffer, 0);
        }

        /// <summary>
        /// Reads a DateTime from the serialization stream.
        /// </summary>
        public DateTime ReadDateTime()
        {
            long value = ReadInt64();
            return new DateTime(value);
        }

        /// <summary>
        /// Reads a TimeSpan from the serialization stream.
        /// </summary>
        public TimeSpan ReadTimeSpan()
        {
            long value = ReadInt64();
            return new TimeSpan(value);
        }

        // Guid requires a 16-byte array.
        private byte[] guidArray = new byte[16];

        /// <summary>
        /// Reads a Guid from the serialization stream.
        /// </summary>
        public Guid ReadGuid()
        {
            Stream.Read(guidArray, 0, 16);
            return new Guid(guidArray);
        }

        /// <summary>
        /// Reads a Char from the serialization stream.
        /// </summary>
        public char ReadChar()
        {
            Stream.Read(arrayBuffer, 0, 2);
            return BitConverter.ToChar(arrayBuffer, 0);
        }

        /// <summary>
        /// Reads a Decimal from the serialization stream.
        /// </summary>
        public decimal ReadDecimal()
        {
            int[] newDecimal = new int[4];
            newDecimal[0] = ReadInt32();
            newDecimal[1] = ReadInt32();
            newDecimal[2] = ReadInt32();
            newDecimal[3] = ReadInt32();
            return new Decimal(newDecimal);
        }

        /// <summary>
        /// Reads a double-precision floating point value from the serialization stream.
        /// </summary>
        public double ReadDouble()
        {
            Stream.Read(arrayBuffer, 0, 8);
            return BitConverter.ToDouble(arrayBuffer, 0);
        }

        /// <summary>
        /// Reads a single-precision floating point value from the serialization stream.
        /// </summary>
        public float ReadSingle()
        {
            Stream.Read(arrayBuffer, 0, 4);
            return BitConverter.ToSingle(arrayBuffer, 0);
        }

        /// <summary>
        /// Reads culture info from the serialization stream.
        /// </summary>
        public System.Globalization.CultureInfo ReadCultureInfo()
        {
            return System.Globalization.CultureInfo.InvariantCulture;
        }

        public void Dispose()
        {
            Dispose(true);
        }

        protected virtual void Dispose(bool disposeAll)
        {
            if (Stream != null)
                Stream.Dispose();

            Cache.Dispose();

            GC.SuppressFinalize(this);
        }

        private int ReadStringLength()
        {
#if USE_16_BIT_STRING_LENGTH
            return this.ReadUInt16();
#else
            return this.ReadUInt24();
#endif
        }

        private int ReadTypeInt()
        {
#if USE_ADDITIONAL_TYPES
            return ReadUInt16();
#else
            return ReadByte();
#endif
        }

        public void WriteHashedType(int typeHash)
        {
            // Write a 1-byte hash for this type
#if USE_16_BIT_STRING_LENGTH
            Stream.WriteUInt16(0);
#else
            Stream.WriteUInt24(0);
#endif
#if USE_ADDITIONAL_TYPES
            Stream.WriteUInt16(typeHash);
#else
            Stream.Write((byte)typeHash);
#endif
        }

        public void WriteStringLength(int len)
        {
#if USE_16_BIT_STRING_LENGTH
            Stream.WriteUInt16(len);
#else
            Stream.WriteUInt24(len);
#endif
        }

        public bool ByteCountIndicatesNull(int len)
        {
#if USE_16_BIT_STRING_LENGTH
            if (len == 0xFFFF)
            {
                // null was written.
                return true;
            }
#else
            if (len == 0xFFFFFF)
            {
                // null was written.
                return true;
            }
#endif

            return false;
        }

#if USE_ADDITIONAL_TYPES
        public static void AddAdditionalTypes(IEnumerable<Type> types)
        {
            AdditionalTypes.AddRange(types);

            List<Type> list = new List<Type>(types);
            list.Sort(new Comparison<Type>((a, b) => a.Name.CompareTo(b.Name)));

            int i = 114;
            foreach (var type in list)
            {
                AddType(type, i++);

                if (i == 250)
                    i = 255;
            }
        }
#endif
    }
}
